在本系列文中,所有的程式碼以及測試都可以在 should-i-use-fp-ts 找到,今日的範例放在 src/day-08
並且有習題和測試可以讓大家練習。
延續昨天的話題, Option
有以下三種型別,所有變數經過 Option
處理後,都會是 None
或是 Some<A>
,這樣設計有助於掌握變數當前的狀態並且統一處理不合法的數值。
export type None = { readonly _tag: 'None' };
export type Some<A> = { readonly _tag: 'Some'; readonly value: A };
export type Option<A> = None | Some<A>;
const one = O.of(1) // { readonly _tag: 'Some'; readonly value: 1 };
但在這種情況下要使用 Option
內容的數值會變的比較繁瑣:
const one = O.of(1); // { _tag: 'Some', value: 1 }
type IncO = (x: O.Option<number>) => O.Option<number>;
const incO: IncO = x => x._tag === 'None' ? O.none : O.some(x.value + 1);
const two = pipe(
one, // { _tag: 'Some', value: 1 }
incO, // { _tag: 'Some', value: 2 }
);
每一個處理都需要做一個 Option
的版本才能享受到使用 Option
的好處但會讓程式碼變的繁複,所以我們需要建立一個函數 map
來處理 將數值從 Option 拿出來,並且將結果放回 Option 的容器之中
的情境,以下就是 O.map 的實作
。
export type Map = <A, B>(f: (a: A) => B) => (x: Option<A>) => Option<B>;
export const map: Map = f => x => x._tag === 'None' ? none : some(f(x.value));
O.map
會接收一個函數 (f: A => B), 之後接收到變數 (x: A) 便開始運算,如果 x
是 None
則回傳 none
,x
是 Some
則將 f(x.value)
的結果放入 some
之中,如此一來就可以使用原始的 function 來處理上面的情況。
const twoMap = pipe(
one, // { _tag: 'Some', value: 1 }
O.map(x => x + 1), // { _tag: 'Some', value: 2 }
);
const noneCase = pipe(
O.none, // { _tag: 'None' }
O.map(x => x + 1), // { _tag: 'None' }
);
如此一來就可以在確保每一步過程合法的情況下不斷運算下去,下面是一個範例:
期末考調分:每個人的份數為 1.2
倍後四捨五入,超過 100
分則以 100
分計算,調分後不足 60
分則視為 None
。
type IsFailed = (x: O.Option<number>) => O.Option<number>;
const isFailed: IsFailed = (x) => {
if (x._tag === 'None') return O.none;
return x.value > 60 ? O.some(x.value) : O.none;
};
type AdjustScore = (x: number) => O.Option<number>;
const adjustScore: AdjustScore = flow( // use 40 as an example
O.of, // { _tag: 'Some', value: 40 }
O.map(x => x * 1.2), // { _tag: 'Some', value: 48 }
isFailed, // { _tag: 'None' }
O.map(Math.round), // { _tag: 'None' }
O.map(x => x > 100 ? 100 : x), // { _tag: 'None' }
);
const studentA = adjustScore(40); // { _tag: 'None' }
const studentB = adjustScore(60); // { _tag: 'Some', value: 72 }
const studentC = adjustScore(100); // { _tag: 'Some', value: 100 }
這邊可以看到 isFailed
特別獨立出來寫而不是使用 O.map
,這是因為在這裡使用 O.map
的話,會導致中途有 Option
嵌套,而無法順利運算下去,等到明天學 flatMap
之後就可以處理的更加漂亮。
/**
* We can not use `O.map` to implement `adjustScore2`
* because `O.map` will wrap the result in `Option` again.
*/
const adjustScore2: AdjustScore = flow(
O.of, // Option<number>
O.map(x => x * 1.2), // Option<number>
O.map(x => x > 60 ? O.some(x) : O.none), // Option<Option<number>>
O.map(Math.round), // never: type mismatch
O.map(x => x > 100 ? 100 : x), // never
);
今天的主題在 should-i-use-fp-ts src/day-08
有習題和測試可以練習。